• Anna Jurkiewicz 116679

  • Dominika Kaczmarska 116977

studia niestacjonarne sobotnio - niedzielne

Projekt z przedmiotu "Podstawy Aproksymacji - od analizy Fouriera po deep learning"

Wstęp

Słuchając muzyki, często ograniczamy się do słuchania sprawdzonej playlisty. Ograniczając się do lubianej playlisty, ciężko śledzić nowości, a przesłuchane piosenki X razy zaczynają się nam nudzić.

A co gdyby móc z playlisty zawierającej nowości wybrać tylko te, które są zgodne z naszym gustem? Oszczędzimy czas spędzony na omijaniu piosenek, które po przesłuchania kawałka nie do końca są w naszym guście, a do tego będziemy mogli ciągle znajdować piosenki, które będą miłe dla naszego ucha.

Zbiór danych

Aby stworzyć model, który będzie wybierał piosenki, które nam się spodobają, potrzebujemy dwa zbiory:

  • utworów, które nam się podobają
  • utworów, które zupełnie nie są w naszym guście

W celu pozyskania danych korzystamy z danych, które możemy pobrać z naszego konta na Spotify. Oprócz informacji takich jak tytuł i wykonawca, Spotify udostępnia wiele parametrów piosenek, np. taneczność, głośność, muzykalność.

Jako piosenki, które nam się spodobają, najłatwiej wziąć zbiór piosenek 'zaserduszkowanych'. Jeśli chodzi o zbiór utworów, które nie są w naszym guście postanowiłyśmy kierować się dwoma gatunkami: Disco i Heavy Metal. Gatunki się bardzo różnią od siebie, aczkolwiek są tak samo nie lubiane przez nas.

Spotify umożliwia pobieranie tych playlist poprzez stworzenie aplikacji nadającej dostęp do konta, zapewnionej przez "Spotify for Dewelopers". Niestety Web API Wrappers w Julii jest jeszcze w fazie testów, więc w celu pobrania danych skorzystałyśmy z Pythona. Wygenerowane dane wgrywamy do Julii jako CSV.

In [1]:
using DataFrames
using CSV
using StatsBase
In [2]:
ulubione = CSV.read("C:\\Users\\domi\\Desktop\\studia_sgh\\sem2\\ulubione.csv", DataFrame);
disco = CSV.read("C:\\Users\\domi\\Desktop\\studia_sgh\\sem2\\disco.csv", DataFrame);
metal = CSV.read("C:\\Users\\domi\\Desktop\\studia_sgh\\sem2\\metal.csv", DataFrame);

Zobaczmy jak wygląda struktura takiego zbioru

In [3]:
describe(ulubione)
Out[3]:

19 rows × 7 columns (omitted printing of 2 columns)

variablemeanminmedianmax
SymbolUnion…AnyUnion…Any
1Column10.000.00
2urispotify:track:018Idkvf82hi44UZmIXiGBspotify:track:7yq4Qj7cqayVTp3FF9CWbm
3name(I Just) Died In Your ArmsŚwit (feat. Daria Zawiałow, KRÓL & Igo)
4artist24kGoldnsanah
5album13 Reasons Why (Season 2)Świt (feat. Daria Zawiałow, KRÓL & Igo)
6popularity43.8392052.089
7danceability0.6220180.1880.6460.953
8energy0.5722160.05490.5880.971
9key5.3618106.011
10loudness-8.07771-20.822-7.418-2.588
11mode0.53768801.01
12speechiness0.06784150.02370.04330.639
13acousticness0.3569933.76e-50.27550.989
14instrumentalness0.0241990.02.22e-60.862
15liveness0.1794970.03630.1170.987
16valence0.4946420.03930.48250.973
17tempo119.12353.863119.557206.042
18duration_ms2.07947e5992272.03214e5442733
19time_signature3.939714.05

Parametry utworów, których będziemy używać do analizy, to:

  • danceability - taneczność, opisuje jak bardzo piosenka nadaje się do tańczenia
  • energy - energia, opisuje miare percepcji bazującej na intensywności i aktywności
  • speechiness - wypowiedziane słowa- wykrywa obecność w utworze wypowiedzianych słów
  • acousticness - akustyczność - miara, określająca czy piosenka jest akustyczna
  • instrumentalness - instrumentalność, prognozuje, czy utwór nie zawiera wokali
  • tempo - tempo, szacowane tempo utworu w uderzeniach na mnute (BPM)
  • loudness - głośność, ogólna głosność ścieżki w decybelach
  • liveness - żywnotność, wykrywa obecność pbliczności na nagraniu
  • valence - wartościowość, opisuje nastrój utworu

Przygotujmy tabele zawierające te parametry oraz ustalmy ich typy na Float64

In [4]:
ulubione[!, :danceability] = convert(Vector{Float64}, ulubione[!, :danceability]);
ulubione[!, :energy] = convert(Vector{Float64}, ulubione[!, :energy]);
ulubione[!, :speechiness] = convert(Vector{Float64}, ulubione[!, :speechiness]);
ulubione[!, :acousticness] = convert(Vector{Float64}, ulubione[!, :acousticness]);
ulubione[!, :instrumentalness] = convert(Vector{Float64}, ulubione[!, :instrumentalness]);
ulubione[!, :tempo] = convert(Vector{Float64}, ulubione[!, :tempo]);
ulubione[!, :loudness] = convert(Vector{Float64}, ulubione[!, :loudness]);
ulubione[!, :liveness] = convert(Vector{Float64}, ulubione[!, :liveness]);
ulubione[!, :valence] = convert(Vector{Float64}, ulubione[!, :valence]);

ulubione = select(ulubione, :danceability, :energy, :speechiness, :acousticness, :instrumentalness, :tempo, :loudness, :liveness, :valence);
In [5]:
metal[!, :danceability] = convert(Vector{Float64}, metal[!, :danceability]);
metal[!, :energy] = convert(Vector{Float64}, metal[!, :energy]);
metal[!, :speechiness] = convert(Vector{Float64}, metal[!, :speechiness]);
metal[!, :acousticness] = convert(Vector{Float64}, metal[!, :acousticness]);
metal[!, :instrumentalness] = convert(Vector{Float64}, metal[!, :instrumentalness]);
metal[!, :tempo] = convert(Vector{Float64}, metal[!, :tempo]);
metal[!, :loudness] = convert(Vector{Float64}, metal[!, :loudness]);
metal[!, :liveness] = convert(Vector{Float64}, metal[!, :liveness]);
metal[!, :valence] = convert(Vector{Float64}, metal[!, :valence]);

metal = select(metal, :danceability, :energy, :speechiness, :acousticness, :instrumentalness, :tempo, :loudness, :liveness, :valence);
In [6]:
disco[!, :danceability] = convert(Vector{Float64}, disco[!, :danceability]);
disco[!, :energy] = convert(Vector{Float64}, disco[!, :energy]);
disco[!, :speechiness] = convert(Vector{Float64}, disco[!, :speechiness]);
disco[!, :acousticness] = convert(Vector{Float64}, disco[!, :acousticness]);
disco[!, :instrumentalness] = convert(Vector{Float64}, disco[!, :instrumentalness]);
disco[!, :tempo] = convert(Vector{Float64}, disco[!, :tempo]);
disco[!, :loudness] = convert(Vector{Float64}, disco[!, :loudness]);
disco[!, :liveness] = convert(Vector{Float64}, disco[!, :liveness]);
disco[!, :valence] = convert(Vector{Float64}, disco[!, :valence]);

disco = select(disco, :danceability, :energy, :speechiness, :acousticness, :instrumentalness, :tempo, :loudness, :liveness, :valence);

Ze względu na to, że parametry mają różne zbiory wartości, wszystkie kolumny poddamy procedurze standaryzacji

In [7]:
ulubione = mapcols(zscore, ulubione);
disco = mapcols(zscore, disco);
metal = mapcols(zscore, metal);

Stwórzmy dwa zbiory lubiane i nie_lubiane, w których dodamy kolumnę like z wartościami:

  • 1 - utwór jest w naszym guście
  • 0 - utwór nie jest w naszym guście
In [8]:
lubiane = ulubione;

insertcols!(lubiane,
    1, 
    :like => 1.0);
In [9]:
first(lubiane, 5)
Out[9]:

5 rows × 10 columns (omitted printing of 3 columns)

likedanceabilityenergyspeechinessacousticnessinstrumentalnesstempo
Float64Float64Float64Float64Float64Float64Float64
11.01.027620.123739-0.189669-0.937936-0.221540.814404
21.0-0.1240421.69785-0.416866-0.937936-0.2213410.399336
31.00.4153460.728429-0.474006-1.16369-0.2214920.294777
41.0-0.08759641.15075-0.518901-0.978988-0.2196180.1057
51.00.962022-0.591331-0.08627331.20531-0.22154-1.35484
In [10]:
nrow(lubiane)
Out[10]:
398

Zbiór lubianych zawiera 398 obserwacji.

In [11]:
nie_lubiane = vcat(disco, metal);

insertcols!(nie_lubiane,
    1, 
    :like => 0.0);
In [12]:
first(nie_lubiane, 5)
Out[12]:

5 rows × 10 columns (omitted printing of 3 columns)

likedanceabilityenergyspeechinessacousticnessinstrumentalnesstempo
Float64Float64Float64Float64Float64Float64Float64
10.00.1409360.7805371.24708-0.0620549-0.1322550.307438
20.0-1.500980.999032-0.311232-0.650521-0.12572.5419
30.0-0.1469320.0490516-0.774048-0.670619-0.132255-0.166407
40.01.004540.391045-0.00843213-0.30221-0.131497-0.00902013
50.01.260420.457543-0.746968-0.615605-0.132255-0.330933
In [13]:
nrow(nie_lubiane)
Out[13]:
656

Zbiór nielubianych piosenek zawiera 656 obserwacji.

W celu sprawdzenia, czy te parametry będą przyjmować różne wartości dla lubianych i nielubianych wartości, dla obu zbiorów tworzymy cornetploty oraz patrzymy czy rozkłady dla parametrów są od siebie różne. Ze względu na to, że pokazując wykresy dla wszystkich parametrów tracimy na czytelności, podzielimy parametry na dwie grupy.

In [31]:
using StatsPlots

1.1) Cornerplot zbioru lubiane dla parametrów: acousticness, speechiness, energy, danceability

In [16]:
@df lubiane cornerplot(cols(2:5))
Out[16]:

1.2) Cornerplot zbioru nie_lubiane dla parametrów: acousticness, speechiness, energy, danceability

In [17]:
@df nie_lubiane cornerplot(cols(2:5))
Out[17]:

2.1) Cornerplot zbioru lubiane dla parametrów: instrumentalness, tempo, loudness, liveness, valence

In [18]:
@df lubiane cornerplot(cols(6:10))
Out[18]:

2.2) Cornerplot zbioru nie_lubiane dla parametrów: instrumentalness, tempo, loudness, liveness, valence

In [19]:
@df nie_lubiane cornerplot(cols(6:10))
Out[19]:

Możemy zauważyć, dla każdego zbioru parametry mają inne charakterystyki. W celu potwierdzenia porównujemy podstawowe statystyki obu zbiorów(min,max,mediana,średnia).

In [416]:
describe(lubiane)
Out[416]:

10 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolFloat64Float64Float64Float64Int64DataType
1like1.01.01.01.00Float64
2danceability-1.04746e-15-3.163560.1748082.412540Float64
3energy5.77428e-17-2.482670.07574781.913810Float64
4speechiness1.69323e-16-0.600529-0.3338787.770410Float64
5acousticness2.58029e-17-1.1723-0.2676362.075620Float64
6instrumentalness-1.95265e-18-0.22154-0.221527.670030Float64
7tempo-1.89617e-16-2.43090.0161533.237650Float64
8loudness3.78257e-16-4.219560.2184271.817610Float64
9liveness-2.2853e-16-0.940403-0.4104325.303010Float64
10valence-2.30413e-16-1.95161-0.05204292.050250Float64
In [417]:
describe(nie_lubiane)
Out[417]:

10 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolFloat64Float64Float64Float64Int64DataType
1like0.00.00.00.00Float64
2danceability-2.09013e-16-3.324140.06885143.526250Float64
3energy-5.18217e-16-4.605850.2912971.331530Float64
4speechiness-3.70469e-16-0.978229-0.376815.591310Float64
5acousticness-6.01653e-17-0.732004-0.3718866.421930Float64
6instrumentalness-3.15635e-17-0.494988-0.13225512.78430Float64
7tempo-1.26796e-15-4.66244-0.01347223.366530Float64
8loudness5.7542e-17-6.869310.1881791.937380Float64
9liveness-1.94966e-16-1.23768-0.3447385.052320Float64
10valence-4.93846e-16-4.399030.1904552.51480Float64

Możemu zauważyć w obu grupach statystyki różnią się od siebie.

Model

Aby zbudować model, łączymy nasze zbiory w jeden data frame.

In [14]:
df = vcat(nie_lubiane, lubiane);

Ze względu na to, że nasz zbiór ma mniejszą liczbę 'jedynek' (utwór, które lubimy) od 'zer' (utwór, których nie lubimy) dzieląc zbiór na zbiór uczący i testowy użyjemy funkcji stratifiedobs. Dzięki niej mamy gwarancję, że proporcja jedynek do zer w obu zbiór będzie taka sama. Zbiory dzielimy w proporcji 70:30 (uczący:testowy)

In [93]:
using MLDataUtils
using Flux
using Statistics
using LinearAlgebra
using Metrics

using Flux: crossentropy, onecold, onehotbatch, params, train!

(X_train,y_train), (X_test,y_test) = stratifiedobs((df[:, 2:end], df[!, :like]), p = 0.7);
In [94]:
X_train = Matrix(X_train)';
X_test = Matrix(X_test)';
In [95]:
y_train = onehotbatch(y_train, 0:1)
Out[95]:
2×738 OneHotMatrix(::Vector{UInt32}) with eltype Bool:
 ⋅  1  ⋅  1  1  1  1  1  1  1  1  1  1  …  1  1  1  1  1  1  1  1  1  1  1  1
 1  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
In [96]:
y_test = onehotbatch(y_test, 0:1)
Out[96]:
2×316 OneHotMatrix(::Vector{UInt32}) with eltype Bool:
 1  1  1  ⋅  1  1  1  1  ⋅  1  ⋅  1  1  …  1  ⋅  1  ⋅  1  1  1  ⋅  1  1  ⋅  ⋅
 ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅  1  ⋅  1  ⋅  ⋅     ⋅  1  ⋅  1  ⋅  ⋅  ⋅  1  ⋅  ⋅  1  1

Model, którymy się posłużymy to sieć klasyfikacyjna. Jest to odmiana sieci neuronowej, w której sygnały wyjściowe mają chrakter jakościowy.

Struktura obejmue warstwę wejściową, warstawę ukrytą oraz warstwe wyjściową. Za funckje aktywacji dla warstwy wejściowej i ukrytej przyjmujemy funkcje relu6. relu6(x)=min(max(0,x),6).

Warstwa wejściowa zawiera 9 neuronów, warstwa ukryta posiada 4 neurony oraz warstwa wyjściowa 2 neurony. Połączenie(dense) warstwy wejściowej z warstwą ukrytą daje nam 40 parametrów, a połączenie warstwy ukrytej z warstwą wyjściową daje nam 10 parametrów.

Dla warstwy wyjściowej przyjmujemy funckje aktywacji - softmax.

In [97]:
model = Chain(
    Dense(9, 4, relu6),
    Dense(4 ,2),
    softmax
)
Out[97]:
Chain(
  Dense(9 => 4, relu6),                 # 40 parameters
  Dense(4 => 2),                        # 10 parameters
  NNlib.softmax,
)                   # Total: 4 arrays, 50 parameters, 456 bytes.

Za funkcje straty przyjmujemy binary_focal_loss.

In [98]:
loss(x, y)= Flux.binary_focal_loss(model(x), y)
Out[98]:
loss (generic function with 1 method)
In [99]:
loss(X_train, y_train)
Out[99]:
0.7278780151512113

Dla naszej sieci wagi wybierane są z góry losowo.

In [100]:
ps = params(model)
Out[100]:
Params([Float32[0.34076843 -0.5061116 … 0.44952604 -0.45178792; 0.28195462 -0.64783144 … 0.09097519 -0.59088284; -0.0049434323 0.20113994 … 0.6676843 0.013157353; -0.18410648 0.17132644 … 0.4602443 -0.21729957], Float32[0.0, 0.0, 0.0, 0.0], Float32[-0.59071755 -0.20811641 0.19414532 0.8807962; 0.62621593 0.96681046 -0.6072341 -0.6091893], Float32[0.0, 0.0]])

Za optymalizator przyjmujemy funkcję NADAM - Nesterovą odmiane ADAMa. Parametry nie wymagają dostrajania.

In [101]:
opt = NADAM()
Out[101]:
NADAM(0.001, (0.9, 0.999), 1.0e-8, IdDict{Any, Any}())

Jako że neurony w warstwie wyjściowej przyjmują wartości binarne, do policzenia wartości dokładności możemy użyć średniej

In [102]:
accuracy(x, y) = mean(onecold(model(x)).==onecold(y))
Out[102]:
accuracy (generic function with 1 method)
In [103]:
accuracy(X_train, y_train)
Out[103]:
0.521680216802168
In [104]:
loss_history = []
Out[104]:
Any[]
In [105]:
epochs = 200
Out[105]:
200

Przechodzimy do trenowania sieci

In [106]:
for epoch in 1:epochs
    train!(loss, ps, [(X_train, y_train)], opt)
    train_loss = loss(X_train, y_train)
    push!(loss_history, train_loss)
end

Sprawdźmy jak wygląda funkcja straty w modelu podczas jego uczenia

In [107]:
Plots.plot(1:epochs, loss_history,
    xlabel = "Epchos",
    ylabel = "Loss",
    title = "Learning curve",
    legend = false)
Out[107]:

Sprawdżmy wartość dokładności na zbiorze uczącym

In [108]:
accuracy(X_train, y_train)
Out[108]:
0.573170731707317

Oraz na zbiorze testowym

In [109]:
accuracy(X_test, y_test)
Out[109]:
0.5791139240506329

Model ten nie jest zadowalający, więc stworzymy model w którym budowa warst będzie taka sama. Nastomiast dla warstwy wyjściowej przyjmujemy sigmoidalną funkcje aktywacji (znormalizowana funkcja wykładnicza). Za funkcje straty przyjmujemy binarną entropie krzyżową.

In [110]:
model2 = Chain(
    Dense(9, 4, relu6),
    Dense(4 ,2),
    softmax
)
Out[110]:
Chain(
  Dense(9 => 4, relu6),                 # 40 parameters
  Dense(4 => 2),                        # 10 parameters
  NNlib.softmax,
)                   # Total: 4 arrays, 50 parameters, 456 bytes.
In [111]:
loss2(x, y)= Flux.mse(model2(x), y)
loss2(X_train, y_train)
Out[111]:
0.28541474156499314
In [112]:
ps2 = params(model2)
opt2 = NADAM()
accuracy2(x, y) = mean(onecold(model2(x)).==onecold(y));
In [113]:
accuracy2(X_train, y_train)
Out[113]:
0.5663956639566395
In [114]:
loss_history2 = []
epochs = 400

for epoch in 1:epochs
    train!(loss2, ps2, [(X_train, y_train)], opt2)
    train_loss = loss2(X_train, y_train)
    push!(loss_history2, train_loss)
end
In [115]:
Plots.plot(1:epochs, loss_history2,
    xlabel = "Epchos",
    ylabel = "Loss",
    title = "Learning curve",
    legend = false)
Out[115]:
In [116]:
accuracy2(X_train, y_train)
Out[116]:
0.6341463414634146
In [117]:
accuracy2(X_test, y_test)
Out[117]:
0.6234177215189873

Obserwujemy, że po zmienie funkcji straty uzyskujemy lepsze wyniki. Kolejną modyfikacją będzie zmiana budowy naszej sieci. Wprowadzamy dodatkową warstwe ukrytą.

In [118]:
model3 = Chain(
    Dense(9, 6, relu6),
    Dense(6, 4),
    Dense(4, 2),
    softmax
)
Out[118]:
Chain(
  Dense(9 => 6, relu6),                 # 60 parameters
  Dense(6 => 4),                        # 28 parameters
  Dense(4 => 2),                        # 10 parameters
  NNlib.softmax,
)                   # Total: 6 arrays, 98 parameters, 776 bytes.
In [119]:
loss3(x, y)= Flux.mse(model3(x), y)
loss3(X_train, y_train)
Out[119]:
0.28679386502302057
In [120]:
ps3 = params(model3)
opt3 = NADAM(0.01)
accuracy3(x, y) = mean(onecold(model3(x)).==onecold(y));
In [121]:
accuracy3(X_train, y_train)
Out[121]:
0.5094850948509485
In [122]:
loss_history3 = []
epochs = 300

for epoch in 1:epochs
    train!(loss3, ps3, [(X_train, y_train)], opt3)
    train_loss = loss3(X_train, y_train)
    push!(loss_history3, train_loss)
end
In [123]:
Plots.plot(1:epochs, loss_history3)
Out[123]:
In [124]:
accuracy3(X_train, y_train)
Out[124]:
0.9065040650406504
In [125]:
accuracy3(X_test, y_test)
Out[125]:
0.8259493670886076

Ten model posiada już zadowolające nas wyniki, użyjemy go do sprawdzenia, które piosenki w danej playliście mogą nam się spodobać. Wgrajmy dane o playliście "New music Friday Polska" oraz zastosujmy przekształcenia takie, jak na zbiorach uczących.

In [136]:
new_music_polska_raw = CSV.read("C:\\Users\\domi\\Desktop\\studia_sgh\\sem2\\new_music_polska.csv", DataFrame);
In [137]:
new_music_polska = new_music_polska_raw;
In [138]:
new_music_polska[!, :danceability] = convert(Vector{Float64}, new_music_polska[!, :danceability]);
new_music_polska[!, :energy] = convert(Vector{Float64}, new_music_polska[!, :energy]);
new_music_polska[!, :speechiness] = convert(Vector{Float64}, new_music_polska[!, :speechiness]);
new_music_polska[!, :acousticness] = convert(Vector{Float64}, new_music_polska[!, :acousticness]);
new_music_polska[!, :instrumentalness] = convert(Vector{Float64}, new_music_polska[!, :instrumentalness]);
new_music_polska[!, :tempo] = convert(Vector{Float64}, new_music_polska[!, :tempo]);
new_music_polska[!, :loudness] = convert(Vector{Float64}, new_music_polska[!, :loudness]);
new_music_polska[!, :liveness] = convert(Vector{Float64}, new_music_polska[!, :liveness]);
new_music_polska[!, :valence] = convert(Vector{Float64}, new_music_polska[!, :valence]);

new_music_polska = select(new_music_polska, :danceability, :energy, :speechiness, :acousticness, :instrumentalness, :tempo, :loudness, :liveness, :valence);
In [139]:
new_music_polska = mapcols(zscore, new_music_polska);
In [140]:
new_music_polska = Matrix(new_music_polska)' ;
In [141]:
likes = model3(new_music_polska)
Out[141]:
2×100 Matrix{Float64}:
 0.853952  0.0290526  0.0993028  …  0.95541    0.981561   0.99049
 0.146048  0.970947   0.900697      0.0445903  0.0184391  0.00950969
In [142]:
likes = onecold(likes).-1
Out[142]:
100-element Vector{Int64}:
 0
 1
 1
 1
 0
 1
 1
 1
 0
 0
 1
 0
 0
 ⋮
 1
 0
 0
 0
 0
 1
 0
 0
 0
 0
 0
 0

Powstały wektor dodajemy do pierwotnego zbioru z tytułami, filtrujemy po jedynkach. Za pomocą modelu dostajemy 38 utworów, które powinny nam się spodobać

In [152]:
likes_new_polska = hcat(new_music_polska_raw, likes);
In [157]:
select(filter(:x1 => ==(1), likes_new_polska), :name, :artist)
Out[157]:

38 rows × 2 columns

nameartist
StringString31
1BREAK MY SOULBeyoncé
2Easy RiderWhite 2115
3SharksImagine Dragons
4PrzypadkiAnia Leon
5The DropDimitri Vegas
6KFCOtsochodzi
7To Też O TobieIgo
8OverdosedTYNSKY
9#fejmbryska
10Not EAZY LifeKukon
11DisasterConan Gray
12IskryBovska
13PsychicChris Brown
14Cracker Island (feat. Thundercat)Gorillaz
15JestemCatz 'n Dogz
16From The D 2 The LBC (with Snoop Dogg)Eminem
17deep in the woodsHayley Kiyoko
18hollaback bitch (with Shygirl & Channel Tres)Mura Masa
19Zielona bluzaWiktoria Zwolińska
20Nie dotykajDJ BRK
21Her Body Is BibleFLETCHER
22Dwie TwarzeGverilla
23Bez LimituOska030
24Bad Ass BitchesWiz Khalifa
25pogobbno$
26AUTOBUSKaen
27Lost MeGiveon
28Blue EyeLa Giang
29SHOWING OFF HER BODY (with Davido)DaBaby
30Bad For Me (feat. Teddy Swims)Meghan Trainor

Powstała playlista została przez nas przesłuchana i utwory przypadły do gustu - jednak może mieć na to wpływ fakt, że lubimy muzykę radiową.

Sprawdźmy jeszcze jak nasz model oceni nasze zainteresowanie gatunkiem, którego też nie jesteśmy wielkim fanem - RAP (aczkolwiek już prędzej go posłuchamy, niż heavy metal lub disco polo)

In [302]:
rap = CSV.read("C:\\Users\\domi\\Desktop\\studia_sgh\\sem2\\rap.csv", DataFrame);
In [303]:
rap[!, :danceability] = convert(Vector{Float64}, rap[!, :danceability]);
rap[!, :energy] = convert(Vector{Float64}, rap[!, :energy]);
rap[!, :speechiness] = convert(Vector{Float64}, rap[!, :speechiness]);
rap[!, :acousticness] = convert(Vector{Float64}, rap[!, :acousticness]);
rap[!, :instrumentalness] = convert(Vector{Float64}, rap[!, :instrumentalness]);
rap[!, :tempo] = convert(Vector{Float64}, rap[!, :tempo]);
rap[!, :loudness] = convert(Vector{Float64}, rap[!, :loudness]);
rap[!, :liveness] = convert(Vector{Float64}, rap[!, :liveness]);
rap[!, :valence] = convert(Vector{Float64}, rap[!, :valence]);

rap = select(rap, :danceability, :energy, :speechiness, :acousticness, :instrumentalness, :tempo, :loudness, :liveness, :valence);
In [304]:
rap = mapcols(zscore, rap);
In [305]:
rap = Matrix(rap)';
In [306]:
like_rap = model3(rap);
In [307]:
like_rap = onecold(like_rap).-1
Out[307]:
46-element Vector{Int64}:
 0
 0
 0
 1
 0
 1
 0
 1
 1
 1
 1
 0
 0
 ⋮
 1
 0
 0
 1
 0
 0
 0
 1
 0
 0
 1
 0
In [308]:
mean(like_rap)
Out[308]:
0.2608695652173913

Nasz model twierdzi, że tylko 26% piosenek z wybranej przez nas rapowej playlisty przypadnie nam do gustu - myślimy że ten wynik jest bardzo trafny.

Podsumowanie

W prezentowanym raporcie zmierzyliśmy się z problemem doboru nowych piosenek biorąc pod uwagę nasz gust. Naszym celem było stworzenie modelu, który na podstawie informacji ze Spotify o tym, jakie piosenki lubimy oraz jakich nie, wskaże utwory z konkretnej playlisty które nam się mogą spodobać.

Do tego wykorzystaliśmy sieci klasyfikacyjne. Pierwszy model, który zbudowaliśmy składał się z 1 warstwy wejściowej, 1 warstw ukrytej oraz 1 warstwy wyjściowej. Za funkcje aktywacji na warstwie wejściowej i ukrytej przyjęliśmy funkcje relu6, a za funkcje aktywacji na warstwie wyjściowej softmax. Wykorzystaną funkcją straty była funkcja binary_focal_loss, optymizatorem funkcja NADAM. Dokładność modelu na zbiorze uczącym wyniosła 0.573 a na zbiorze testowym 0.579. Wynik ten nas nie zadowolił dlatego stworzyliśmy kolejny model oparty na sieci o tych samych warstwach.

Model 2 składał się również z 1 warstwy wejściowej, 1 warstw ukrytej oraz 1 warstwy wyjściowej. Za funkcje aktywacji na warstwie wejściowej i ukrytej przyjęliśmy funkcje relu6, za funkcje aktywacji na warstwie wyjściowej softmax, a za optymizator NADAM(). Za funkcje starty przyjęliśmy MSE. Dokładność modelu na zbiorze uczącym wyniosła 0.634, a na zbiorze testowym 0.6234. Zatem po zmianie funkcji starty - uzyskaliśmy lepsze wyniki.

W modelu 3 wprowadziliśmy modyfikacje do struktury naszej sieci i dodaliśmy 1 warstwę ukrytą. Pozostałe funkcje aktywacji oraz straty pozostały bez zmian. W optymizatorze domyślny parametr 0.001 zamieniliśmy na 0.01. Otrzymane wartości dokładności to 0.90 na zbiorze uczącym się oraz 0.82 na zbiorze testowym. Takie wyniki zostały uznane przez nas za satysfakcjonujące.

Następnie model 3 wykorzystaliśmy do tego, aby z playlisty z nowymi utworami - New music friday Polska- zostały wybrane utwory, które mogą nam się spodobać. Model słusznie trafił w nasze gusta.

Kolejnym krokiem było przekonanie się jak nasz model oceni sympatię do gatunku, który raczej nie należy do preferowanych przez nas. Model pokazał, że 26% piosenek z playlisty z muzyką z gatunku rap może nam się spodobać. Jest to jak najbardziej zgodne z naszym gustem oraz muzyką jakiej słuchamy przeważnie.

Podsumowując oceniamy model 3 jako dobry i satysfakcjonujący. Model ten poprawnie wybrał nowe, potencjalnie ulubione piosenki oraz trafnie ocenił zainteresowanie nowym gatunkiem.